//===============================================================================
// Microsoft Architecture Strategy Team
// LitwareHR - SaaS Sample Application
//===============================================================================
// Copyright  Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===============================================================================


namespace Shp.Runtime.Sitka.ServiceAgent
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Diagnostics;
    using System.ServiceModel;
    using System.Globalization;
    using System.Collections.ObjectModel;
    using Shp.Runtime.Sitka.ServiceAgent.Entities;
    using Shp.Runtime.Sitka.ServiceAgent.EntityMappers;
    using System.Configuration;
    using Shp.Runtime.Sitka.ServiceAgent.Managers;
    using Microsoft.Practices.EnterpriseLibrary.Caching;
    using Shp.Runtime.Sitka.ServiceGateway.SitkaClient;

    /// <summary>
    /// A Repository for typed entities
    /// </summary>
    /// <typeparam name="T">T is any IEntity (see inetrface)</typeparam>
    public class Repository<T> : IDisposable where T : IEntity, new()
    {
        private const string QueryBase = @"from e in entities {0} select e";
        private ISitkaSoapService proxy;
        private string tenantId;
        private string entityKind = typeof(T).ToString();
        private EntityMapper<T> mapper;
        private IRepositoryCacheManager cacheManager;
        private ITenantManager tenantManager;

        /// <summary>
        /// Builds a new Repository class with optional caching and a tenant manager
        /// </summary>
        /// <param name="tenantId">tenantId. Will be later used to map to AuthorityId/ContainerId</param>
        /// <param name="entityMapper">The mapper translated between IEntities and Flexible Entities</param>
        /// <param name="cacheManager">The name of the cache manager. Null means no caching</param>
        /// <param name="tenantManager">Manages mapping between tenantId and AuthorityId/ContainerId</param>
        public Repository(string tenantId, EntityMapper<T> entityMapper, IRepositoryCacheManager cacheManager, ITenantManager tenantManager)
        {
            this.tenantId = tenantId;
            this.mapper = entityMapper;
            //this.mapper = new EntityMapperT<T>();
            proxy = SitkaProxyFactory.BuildProxy(tenantManager);
            this.cacheManager = cacheManager;
            this.tenantManager = tenantManager;
        }

        /// <summary>
        /// Builds a new Repository class with optional caching
        /// </summary>
        /// <param name="tenantId">tenantId. Will be later used to map to AuthorityId/ContainerId</param>
        /// <param name="entityMapper">The mapper translated between IEntities and Flexible Entities</param>
        /// <param name="cacheManager">The name of the cache manager. Null means no caching</param>
        public Repository(string tenantId, EntityMapper<T> entityMapper, IRepositoryCacheManager cacheManager)
            : this(tenantId, entityMapper, cacheManager, new DefaultTenantManager())
        {
        }

        /// <summary>
        /// Constructor with no caching enabled
        /// </summary>
        /// <param name="tenantId">tenantId. Will be later used to map to AuthorityId/ContainerId</param>
        /// <param name="entityMapper">The mapper translated between IEntities and Flexible Entities</param>
        public Repository(string tenantId, EntityMapper<T> entityMapper)
            : this(tenantId, entityMapper, null)
        {
        }

        /// <summary>
        /// Closes the proxy (nothing for the offline proxy)
        /// </summary>
        public void Close()
        {
            if (proxy != null)
            {
                if (proxy is SitkaSoapServiceClient)
                {
                    (proxy as SitkaSoapServiceClient).Close();
                }
            }
        }

        /// <summary>
        /// Creates a new instance of T in Sitka, optionally stores it in local cache for retrieval if caching is enabled
        /// </summary>
        /// <param name="entity">The entity to store in Sitka</param>
        public void Insert(T entity)
        {
            try
            {
                Scope scope = CreateScope();

                Entity flexibleEntity = mapper.FromType(entity);

                proxy.Create(scope, flexibleEntity);

                //Only entities marked as "cacheable" go to the cache 
                if (entity is ICacheableEntity)
                {
                    AddOrUpdateEntityFromCache(flexibleEntity);
                }
            }
            catch (FaultException<Error> ex)
            {
                throw new InsertException( ex );
            }
        }

        /// <summary>
        /// Eliminates an entity from Sitka and from the cache given its Id
        /// </summary>
        /// <param name="Id">The Id of the entity to eliminate</param>
        public void Delete(Guid id)
        {
            Scope scope = CreateScope();
            scope.EntityId = id.ToString();
            try
            {
                proxy.Delete(scope);
                DeleteEntityFromCache(scope.EntityId);
            }
            catch (FaultException<Error> ex)
            {
                throw new DeleteException(ex);
            }
        }

        /// <summary>
        /// Get an instance from Sitka or cache if cache is enabled
        /// </summary>
        /// <param name="Id">The Id of the entity to retrieve</param>
        /// <returns>T the entity instance. If T is cacheabkle and caching is enabled, the isCached flag will be set accordingly</returns>
        public T GetById(Guid id)
        {
            T t = default(T);
            Scope scope = CreateScope();
            bool isCached = true;
        
            try
            {
                Entity entity = GetEntityFromCache(id.ToString());

                if (entity != null)
                {
                    isCached = true;
                }
                else
                {
                    List<Entity> l = proxy.Query(scope, @"from e in entities where e.Id==""" + id.ToString() + @""" select e");

                    if (l.Count == 0)
                    {
                        return default(T);
                    }

                    isCached = false;
                    entity = l[0];
                    AddOrUpdateEntityFromCache(entity);
                }

                t = mapper.ToType(entity);

                ICacheableEntity cacheableEntity = t as ICacheableEntity;

                if (cacheableEntity != null )
                {
                    cacheableEntity.IsCached = isCached;
                }

                return t;
            }
            catch (FaultException<Error> ex)
            {
                throw new GetException(ex);
            }
        }

        /// <summary>
        /// Searches the container for matches against the specificed query
        /// </summary>
        /// <param name="query">The query statement</param>
        /// <returns>An enumeration of T's</returns>
        public IEnumerable<T> Search(string query)
        {
            Scope scope = CreateScope();
            string queryString = String.Format(CultureInfo.InvariantCulture, QueryBase, query);
            bool isCached = true;

            Collection<Entity> list = GetQueryResultsFromCache(queryString);

            if (list == null)
            {
                isCached = false;
                list = new Collection<Entity>(proxy.Query(scope, queryString));
                AddQueryResultsToCache(queryString, list);
            }

            foreach (Entity e in list)
            {
                yield return MapEntityMarkCache(e, isCached);
            }
        }

        /// <summary>
        /// Get all entitites on a container of a given type (T)
        /// </summary>
        /// <returns>An enumeration of all instances of T's</returns>
        public IEnumerable<T> LoadAll()
        {
            return Search(string.Format(CultureInfo.InvariantCulture, @"where e.Kind==""{0}""", entityKind));
        }


        /// <summary>
        /// Updates an entity
        /// </summary>
        /// <param name="entity">The entity to update</param>
        public void Update(T entity)
        {
            try
            {
                Scope scope = CreateScope();
                scope.EntityId = entity.Id.ToString();
                Entity flexibleEntity = mapper.FromType(entity);
                proxy.Update(scope, flexibleEntity);
                AddOrUpdateEntityFromCache(flexibleEntity);
            }
            catch (FaultException<Error> ex)
            {
                throw new UpdateException(ex);
            }
        }


        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                Close();
            }
        }
        
        #endregion

        #region CachingHelpers

        private void AddOrUpdateEntityFromCache(Entity entity)
        {
            if (cacheManager != null)
            {
                cacheManager.AddOrUpdateEntity(tenantId, entity);
            }
        }

        private void DeleteEntityFromCache(string id)
        {
            if (cacheManager != null)
            {
                cacheManager.DeleteEntity(tenantId, entityKind, id);
            }
        }

        private Entity GetEntityFromCache(string id)
        {
            if (cacheManager == null)
            {
                return null;
            }

            return cacheManager.GetEntityFrom(tenantId, entityKind, id);
        }

        private void AddQueryResultsToCache(string queryString, Collection<Entity> list)
        {
            if (cacheManager != null)
            {
                cacheManager.AddQueryResults(tenantId, queryString, list);
            }
        }

        private Collection<Entity> GetQueryResultsFromCache(string queryString)
        {
            if (cacheManager == null)
            {
                return null;
            }

            return cacheManager.GetQueryResults(tenantId, queryString);
        }
        #endregion

        #region Various helper methods

        private Scope CreateScope()
        {
            Scope s = new Scope();
            s.AuthorityId = tenantManager.GetAuthorityId(tenantId);
            s.ContainerId = tenantManager.GetContainerId(tenantId);
            return s;
        }

        private T MapEntityMarkCache(Entity e, bool isCached)
        {
            T t = mapper.ToType(e);

            ICacheableEntity cacheableEntity = t as ICacheableEntity;

            if(cacheableEntity != null )
            {
                cacheableEntity.IsCached = isCached;
            }
            return t;
        }

        #endregion
    }
}
